Skip to content

fix(engine): report the cause and attempt count when a node exhausts retries - #77

Merged
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/node-failure-diagnostics
Aug 25, 2026
Merged

fix(engine): report the cause and attempt count when a node exhausts retries#77
M3gA-Mind merged 1 commit into
tinyhumansai:mainfrom
M3gA-Mind:fix/node-failure-diagnostics

Conversation

@M3gA-Mind

@M3gA-Mind M3gA-Mind commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Summary

node failed after retries was emitted with the node id and nothing else:

tracing::warn!(node = %node.id, "node failed after retries");

That is the only line a run produces when a node gives up, so a report of it cannot be root-caused — the log says a node failed, but not why, and not how many attempts it took. This is exactly what happened to tinyhumansai/openhuman#5626, which is a single staging occurrence of this warning and cannot be diagnosed as filed.

The cause was already in hand and simply not named. last_err: Option<EngineError> is a parameter of finish_execution and is in scope at the warning; it is just not unwrapped until the let Some(err) 44 lines further down. This borrows it there (the unwrap below still takes it by value), adds the on_error policy that decides what happens next, and threads attempts_used through from the retry loop so "after retries" carries a number.

tracing::warn!(
    node = %node.id,
    attempts,
    on_error = %on_error,
    error = ?last_err,
    "node failed after retries"
);

Deliberately logged at the existing site rather than after the unwrap: the None arm below it is the defensive unreachable path, and moving the line past it would stop that case reporting at all.

What this is not

This is a diagnostic prerequisite, not a root-cause fix. It does not explain why any particular node failed, and it is not expected to change any observed behaviour. tinyhumansai/openhuman#5626 needs the workflow graph containing the node and a staging log window to go further, neither of which exists in a repository — that issue should stay open, and closing it needs a person, not this PR.

Worth stating for anyone reading the linked issue: [outcome] is not a subsystem and there is no product feature called "the summarize outcome node". [outcome] is the tracing target derived from this module's path — the generic node-completion handler every node passes through — and summarize is just an id a graph author chose.

API Or Behavior Changes

None to any public API. finish_execution is pub(super) and gains one parameter; its only caller is engine/build/activation.rs, updated here.

Behaviour is log-only: no control flow is touched. The failure was already routed independently of this line — the ExecutionStep { status: StepStatus::Error, .. } pushed just below it, observer.on_step_finish, and the continue/route/stop policy after it are all unchanged.

One thing for a reviewer to weigh: EngineError can carry tool and config text, so this is a new place where payload fragments may reach logs. It is at WARN on a path that only runs after every retry has failed, but it is a real change in what gets written, and every downstream consumer inherits it on the next bump.

Tests

New: tests/node_failure_diagnostics_e2e.rs. It registers a hand-rolled global tracing::Subscriber that keeps every event with its fields keyed by name, runs a two-node graph whose tool_call has no slug (the deterministic Capability failure tests/error_recovery_e2e.rs already uses) with retry.max_attempts: 3, and asserts the warning carries node, error, attempts = 3 and on_error = stop.

Two deliberate choices:

  • No new dependency. The subscriber is ~50 lines against tracing itself rather than pulling in tracing-subscriber; this crate takes tracing with default-features = false and carries no subscriber dep. Registered globally rather than via with_default because the engine runs on tokio worker threads and a thread-local dispatcher would miss them — each tests/*.rs is its own binary, so the one global registration is safe.
  • Fields are looked up by name, so the test is not vacuous. Asserting that error merely renders non-empty would still pass if the field were dropped again, because a missing field and an empty one are indistinguishable once formatted.

Proven to fail without the fix. Reverting the warning to its previous form and keeping the test:

---- retries_exhausted_warning_reports_the_cause_and_the_attempt_count stdout ----
thread '...' panicked at tests/node_failure_diagnostics_e2e.rs:180:9:
the retries-exhausted warning carries no `error` field, so nothing in the log
can say why the node failed; fields were {"node": "summarize"}

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out

fields were {"node": "summarize"} is the whole problem in one line.

Commands run locally on this branch, all exit 0:

  • cargo fmt --check
  • cargo clippy --all-targets -- -D warnings
  • cargo clippy --all-targets --all-features -- -D warnings
  • cargo build --all-targets
  • cargo build --all-targets --all-features
  • cargo test — 1376 passed, 0 failed
  • cargo test --all-features — 1421 passed, 0 failed

The full suite matters here rather than a scoped filter: finish_execution's signature changed, so every caller and every test that drives a failing node had to be re-checked, not just the new file.

Documentation

None needed — the rationale for logging at this site rather than after the unwrap is a comment at the call site, where the next person to move the line will read it.

Summary by CodeRabbit

  • Bug Fixes

    • Improved diagnostics when a node fails after exhausting retries.
    • Retry warnings now identify the failed node, number of attempts, configured error-handling policy, and underlying error.
  • Tests

    • Added end-to-end coverage verifying retry-failure details are reported correctly.

…retries

`node failed after retries` was emitted with the node id and nothing else, so
a report of it could not be root-caused: the log said a node failed but not
why, and not how many attempts it took.

`last_err` is already a parameter of `finish_execution` and is in scope at the
warning — it is simply not unwrapped until 44 lines further down. Name it on
the line (borrowed; the unwrap below still takes it by value), along with the
`on_error` policy that decides what happens next, and thread `attempts_used`
through from the retry loop so "after retries" carries a number.

Logged at the existing site rather than after the `let Some(err)` unwrap: the
`None` arm there is the defensive unreachable path, and moving the line past
it would stop that case reporting at all.

Log-only; no control flow changes. The failure was already routed
independently via the `ExecutionStep { status: Error }` pushed just below.
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 0155ba5a-62d2-4b37-977a-258f30d3d7cb

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba0b91 and 31dce6f.

📒 Files selected for processing (3)
  • src/engine/build/activation.rs
  • src/engine/build/outcome.rs
  • tests/node_failure_diagnostics_e2e.rs

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The execution finalization path now receives the executor attempt count and emits structured retries-exhausted warnings. A mock-feature end-to-end test captures tracing events and verifies the node, error, attempt count, and error policy.

Changes

Retries-exhausted diagnostics

Layer / File(s) Summary
Propagate attempts and emit failure details
src/engine/build/activation.rs, src/engine/build/outcome.rs
finish_execution receives the executor attempt count. Node-failure warnings now include the node, error, attempt count, and on_error policy.
Capture and verify diagnostic events
tests/node_failure_diagnostics_e2e.rs
The end-to-end test captures tracing fields and verifies diagnostics for a three-attempt failing tool call.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: 🔵 Low · up to 31dce

The change only adds retry count, policy, and failure details to exhausted-node warnings without changing execution behavior; merge is reasonable with explicit owner awareness that error payload fragments may now appear in production logs.

Suggested reviewers: senamakel

Poem

A rabbit watched the retries hop,
Three thumps echoed, then they stopped.
The node, error, count, and rule
Now dance inside the tracing spool.
“Clear logs!” cried Bun, and twitched his nose.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main logging change: reporting the error cause and attempt count when retries are exhausted.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Warning

Your free Security trial is over. An organization admin can activate billing to continue.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tinysweeper tinysweeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

tinysweeper found nothing blocking. Approving.

$0.0000 · 0 in / 0 out · 390 embedded · openrouter/openai/text-embedding-3-small

@tinysweeper

tinysweeper Bot commented Aug 24, 2026

Copy link
Copy Markdown

How this change flows

1 changed behaviour across 12 relationships. 5 surrounding behaviours are shown (60 graph nodes walked). 39 further behaviours left out to keep the diagram readable.

flowchart LR
  n0["HandlerData<br/>changed"]:::changed
  n1["finish_execution"]:::impacted
  n2["execute"]:::impacted
  n3["Send"]:::impacted
  n4["Node"]:::impacted
  n5["RunObserver"]:::impacted
  n0 -->|uses| n4
  n0 -->|uses| n5
  n1 -->|calls| n3
  n1 -->|uses| n3
  n1 -->|uses| n4
  n1 -->|uses| n5
  n2 -->|calls| n1
  n2 -->|calls| n3
  n2 -->|uses| n3
  n2 -->|calls| n4
  n5 -->|uses| n3
  n5 -->|implements| n3
  classDef changed fill:#0d4429,stroke:#238636,color:#e6edf3
  classDef impacted fill:#161b22,stroke:#6e7681,color:#c9d1d9
  classDef flagged fill:#5a1e02,stroke:#d93f0b,color:#ffffff
  classDef blocking fill:#67060c,stroke:#f85149,color:#ffffff
Loading

Green: changed behaviour. Grey: surrounding behaviour. Arrows name the call, use, implementation, or test relationship. Orange: has findings. Red: has a finding that blocks the merge.

tinysweeper 0.1.0

@tinysweeper tinysweeper Bot added the priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect. label Aug 24, 2026
@M3gA-Mind
M3gA-Mind merged commit 7560cea into tinyhumansai:main Aug 25, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

priority: p3 Whenever. Cosmetic, a nicety, or a cleanup with no user visible effect.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant